package lumis.portal.propertybag.serialization;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.Id;

import lumis.portal.PortalException;
import lumis.portal.PortalObjectNotFoundException;
import lumis.portal.UnexpectedException;
import lumis.portal.dao.hibernate.Hibernate3FieldReplicator;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.manager.ManagerFactoryInternal;
import lumis.portal.propertybag.Property;
import lumis.portal.propertybag.PropertyBag;
import lumis.portal.serialization.BaseSerializationContext;
import lumis.portal.serialization.DeserializationContext;
import lumis.portal.serialization.SerializationContext;
import net.sf.beanlib.BeanlibException;
import net.sf.beanlib.hibernate.HibernateBeanReplicator;
import net.sf.beanlib.hibernate3.Hibernate3BeanTransformer;

import org.hibernate.ReplicationMode;
import org.hibernate.Session;

/**
 * Manager for Acl serialization
 * 
 * @version $Revision: 12710 $ $Date: 2011-03-18 17:00:16 -0300 (Fri, 18 Mar 2011) $
 * @since 4.0.7
 */
public class PropertyBagSerializationManager implements IPropertyBagSerializationManager
{
	// DESERIALIZATION

	public void deserializePropertyBag(DeserializationContext deserializationContext, 
			String propertyBagId, String parentPropertyBagId) throws PortalException
	{
		deserializationContext.logDebug("Deserializing property bag '" + propertyBagId + "'");

		// deserialize property bag
		PropertyBag propertyBag = ManagerFactoryInternal.getSerializationManager2().deserializeObject(
						PropertyBag.class, deserializationContext.getDirectory(), 
						getSerializedFilePath(propertyBagId),
						null, deserializationContext.getDefaultSerializer(), true);
		if (propertyBag == null)
			return;
		
		// generate new identifiers, if it is configured as such
		if (deserializationContext.isCreateNewIds())
			generateNewIdentifiers(deserializationContext, propertyBag, new HashSet<Object>());
		
		// set parent property bag reference
		if (parentPropertyBagId != null)
		{
			parentPropertyBagId = deserializationContext.getIdMapping(parentPropertyBagId);
			PropertyBag parentPropertyBag = ManagerFactory.getPropertyBagManager().get(parentPropertyBagId);
			propertyBag.setParentPropertyBag(parentPropertyBag);
		}
		
		// if existing elements should be deleted, delete all existing properties
		// for the property bag
		if (deserializationContext.isDeleteExistingElements())
		{
			try
			{
				PropertyBag persistedPropertyBag = ManagerFactory.getPropertyBagManager().get(propertyBag.getId());
				Collection<String> propertyNames = new ArrayList<String>(persistedPropertyBag.getLocalPropertyMap().keySet());
				for (String propertyName: propertyNames)
					persistedPropertyBag.removeProperty(propertyName);
				
				// must flush the removal, or problem happens in replicate below
				ManagerFactory.getEntityManager().flush();
			}
			catch (PortalObjectNotFoundException e)
			{
				// property bag not found, just continue
			}
		}
		
		// replicate property bag to persistence
		ReplicationMode replicationMode = deserializationContext.isUpdateIfElementExists() ? 
				ReplicationMode.OVERWRITE : ReplicationMode.IGNORE;
		((Session)ManagerFactory.getEntityManager().getDelegate()).replicate(propertyBag, replicationMode);
		
		// flush due to mix of JPA and JDBC persistence
		ManagerFactory.getEntityManager().flush();
	}

	/**
	 * Generates new identifiers for a JPA entity structure.
	 * The fields with {@link Id} annotation have their value updated. Only
	 * fields of classes with {@link Entity} annotation are analyzed. 
	 * @param deserializationContext the deserialization context.
	 * @param entity the JPA entity, root of the structure.
	 * @param entitiesUpdated tracks the entities that already had their
	 * identifiers updated, to prevent endless loop in structure cycles.
	 * @since 5.0.0
	 */
	private void generateNewIdentifiers(DeserializationContext deserializationContext, 
			Object entity, Set<Object> entitiesUpdated) throws PortalException
	{
		// check if object is an entity
		Class<? extends Object> clazz = entity.getClass();
		if (clazz.getAnnotation(Entity.class) == null)
			return;
		
		// add entity to entitiesUpdated. If already present, does not need
		// to process this entity.
		if (!entitiesUpdated.add(entity))
			return;
		
		// for each field
		for (Field field: clazz.getDeclaredFields())
        {
			// if the field is a JPA identifier field
			if (field.getAnnotation(Id.class) != null)
			{
				// generate new identifier for this field 
				deserializationContext.applyPrimaryKeyFieldIdMappings(entity, new String[] { field.getName() });
			}
			else
			{
				// read value
				Object value = readFieldAsPrivileged(entity, field);
				
				// if value is null, skip it
				if (value == null)
					continue;
				
				// if value is a map, use just map's values
				if (value instanceof Map)
				{
					value = ((Map<?,?>)value).values();
				}
				
				// if iterable, generate identifiers for each item
				if (value instanceof Iterable)
				{
					for (Object obj: ((Iterable<?>)value))
						generateNewIdentifiers(deserializationContext, obj, entitiesUpdated);
				}
				else
				{
					// treat value as an entity and try to generate its identifiers
					generateNewIdentifiers(deserializationContext, value, entitiesUpdated);
				}
			}
        }
	}

    /** 
     * Reads the field value, as a privileged action if necessary.
     * @since 5.0.0 
     */
    private Object readFieldAsPrivileged(final Object target, final Field field)
    {
    	if (Modifier.isPublic(field.getModifiers()))
		{
			try
			{
				return field.get(target);
			}
			catch (IllegalAccessException ex)
			{
				// drop thru to try again
			}
		}
		return AccessController.doPrivileged(new PrivilegedAction<Object>()
		{
			public Object run()
			{
				field.setAccessible(true);
				try
				{
					return field.get(target);
				}
				catch (IllegalArgumentException e)
				{
					throw new BeanlibException(e);
				}
				catch (IllegalAccessException e)
				{
					throw new BeanlibException(e);
				}
			}
		});
    }
    
	// SERIALIZATION

	public void serialize(SerializationContext serializationContext, String propertyBagId) throws PortalException
	{
		// create hibernate bean replicator
		Hibernate3BeanTransformer transformer = Hibernate3BeanTransformer.newBeanTransformer();
		Set<Field> fieldsToIgnore = new HashSet<Field>();
		try
		{
			fieldsToIgnore.add(PropertyBag.class.getDeclaredField("parentPropertyBag"));
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
		transformer.initBeanReplicatable(new Hibernate3FieldReplicator.Factory(fieldsToIgnore));
		HibernateBeanReplicator replicator = new HibernateBeanReplicator(transformer);
		
		// replicate property bag
		PropertyBag propertyBag = ManagerFactory.getPropertyBagManager().get(propertyBagId);
		PropertyBag replicatedBag = replicator.copy(propertyBag);
		
		// serialize replicated property bag
		ManagerFactoryInternal.getSerializationManager2().serializeObject(replicatedBag, 
				serializationContext.getDirectory(), getSerializedFilePath(propertyBagId), 
				null, serializationContext.getDefaultSerializer());
	}

	// GENERAL

	/**
	 * Returns the path for the serialized file of a property bag.
	 * @param propertyBagId the property bag identifier.
	 * @since 5.0.0
	 */
	private String getSerializedFilePath(String propertyBagId)
	{
		return "propertyBags/" + propertyBagId + ".xml";
	}

	public void setupSerializationContext(BaseSerializationContext serializationContext)
	{
		// define aliases
		serializationContext.getSerializationSettings().getClassAliases().put(PropertyBag.class, "propertyBag");
		serializationContext.getSerializationSettings().getClassAliases().put(Property.class, "property");
	}
}
